/**
  C backend for comun transpiler.

  by drummyfish, released under CC0 1.0, public domain
*/

void _constToC(uint64_t n, char *s)
{
  uint8_t bytes = n < 256 ? 1 : 32;

  bytes *= 2;

  s[0] = '0';
  s[1] = 'x';
  s[bytes + 2] = 0;

  for (uint8_t i = 0; i < bytes; ++i)
  {
    uint8_t d = n % 16;
    s[bytes + 1 - i] = (d < 10 ? '0' : ('a' - 10)) + d;
    n /= 16;
  }
}

void _ptrIndexToC(uint32_t ptrIndex, char *s, uint8_t typeEnv)
{
  s[0] = '(';

  if (ptrIndex <= CMN_LAST_SPECIAL_PTR)
  {
    s[1] = 's'; s[2] = 't'; s[3] = '0' + typeEnv;

    if (ptrIndex > 9)
    {
      s[4] = ' '; s[5] = '-'; s[6] = ' '; s[7] = '0'; s[8] = 'x';
      s[9] = 'a' - 10 + ptrIndex; s[10] = ')'; s[11] = 0;
    }
    else if (ptrIndex != 0)
    {
      s[4] = ' '; s[5] = '-'; s[6] = ' '; s[7] = '0' + ptrIndex; s[8] = ')';
      s[9] = 0;
    }
    else
    {
      s[4] = ')'; s[5] = 0;
    }
  }
  else
  {
    s[0] = 'p'; s[1] = '0' + typeEnv; s[2] = '[';
    _constToC(ptrIndex - CMN_LAST_SPECIAL_PTR - 1,s + 3);

    uint8_t lastChar = 0;

    while (s[lastChar] != 0)
      lastChar++;

    s[lastChar] = ']'; s[lastChar + 1] = 0;
  }
}

void _instrArgsToC(const uint8_t *instr, char *x, char *y, int8_t *stackShift)
{
  x[0] = 's';
  x[1] = 't';
  x[2] = '0' + CMN_instrTypeEnv(instr);
  x[3] = '[';
  x[4] = '0';
  x[5] = ']';
  x[6] = 0;

  for (uint8_t i = 0; i < 7; ++i)
    y[i] = x[i];

  *stackShift = 1;

  switch (instr[0] & CMN_MASK_INSTR_MODE)
  {
    case CMN_OPCODE_1C1:
      _constToC(CMN_instrGetConst(instr),x);
      __attribute__((fallthrough));

    case CMN_OPCODE_11:
      if (!(instr[1] & CMN_MASK_INSTR_NOPOP))
        *stackShift -= 1;

      break;

    case CMN_OPCODE_21:
      if (!(instr[1] & CMN_MASK_INSTR_NOPOP))
        *stackShift -= 2;
      __attribute__((fallthrough));

    default:
      y[4] = '-';
      y[5] = '1';
      y[6] = ']';
      y[7] = 0;
      break; 
  }
}

const char *_typeEnvToC(uint8_t typeEnv)
{
  return typeEnv == 0 ? "unsigned int" : (typeEnv == 1 ? "unsigned char" :
    (typeEnv == 2 ? "uint16_t" : "uint32_t"));
}

uint8_t printBytecodeAsC(const uint8_t *bytecode, FILE *f, const CMN_Compiler *compiler)
{
  char is[128]; // indent string

  for (uint8_t i = 0; i < 128; ++i)
    is[i] = ' ';

  is[127] = 0;

  uint8_t needed = 0; // from MSB: 0 0 signfuncs I/O env32 env16 env8 env0
  const uint8_t *instr = bytecode + CMN_BYTECODE_HEADER_SIZE;

  // 1st pass: check what we'll need
  while (*instr != CMN_OPCODE_END)
  {
    if ((!(needed & 0x10)) && (*instr == CMN_OPCODE_OUT ||
        *instr == CMN_OPCODE_INP || *instr == CMN_OPCODE_INU))
      needed |= 0x10;

    if (CMN_instrTouchesMem(*instr) || CMN_instrTouchesPtr(*instr))
      needed |= 0x01 << CMN_instrTypeEnv(instr);

    if (*instr == CMN_OPCODE_DSX || *instr == CMN_OPCODE_DSC ||
        *instr == CMN_OPCODE_MSX || *instr == CMN_OPCODE_MSC ||
        *instr == CMN_OPCODE_PAX)
      needed |= 0x20; 

    if (needed == 0x3f) // no need to continue
      break;

    instr += 2;
  }

  if (needed & 0x10)
    fputs("#include <stdio.h>\n",f);

  if (needed & 0x2c)
    fputs("#include <stdint.h>\n",f);

  if (needed & 0x3c)
    fputc('\n',f);

  if (needed & 0x10)
    fputs(
      " // I/O:\n"
      "unsigned char eof = 0;\n"
      "void out(char c) { putchar(c); }\n"
      "unsigned char in() { int c; if (eof) return 0; c = getchar(); if (c == EOF) { eof = 1; c = 0; } return c; }\n\n",f);

  if (needed & 0x20)
    fputs(
      "// signed/unsigned conversions:\n"
      "unsigned int  s2u0(int x)           { return x; }\n"
      "int           u2s0(unsigned int x)  { return x > ((unsigned int) -1) / 2 ? (-1 * ((long long) ((unsigned int) -1)) - 1) + x : x; };\n"
      "unsigned char s2u1(signed char x)   { return x < 0 ? 0xff + x : x; }\n"
      "signed char   u2s1(unsigned char x) { return x > 127 ? (-1 * ((int) 0xff) - 1) + x : x; };\n"
      "uint16_t      s2u2(int16_t x)       { return x < 0 ? 0xffff + x : x; }\n"
      "int16_t       u2s2(uint16_t x)      { return x > 127 ? (-1 * ((long) 0xffff) - 1) - x : x; };\n"
      "uint32_t      s2u3(int32_t x)       { return x < 0 ? 0xffffffff + x : x; }\n"
      "int32_t       u2s3(uint32_t x)      { return x > 127 ? (-1 * ((long long) 0xffffffff) - 1) - x : x; };\n\n",f);

  fputs("int gargc;\nchar **gargv;\n",f);

  uint32_t mems[4], ptrs[4];
  CMN_estimateMemory(bytecode,128,mems,ptrs); // TODO: 128
  char helpertStr[64];

  for (uint8_t i = 0; i < 4; ++i)
  {
    if (needed & (0x01 << i))
    {
      fprintf(f,"%s m%c[%d], *st%c = m%c; // memory and stack top\n",
        _typeEnvToC(i),'0' + i,mems[i],'0' + i,'0' + i);

      uint8_t first = 1;

      if (ptrs[i] > 1)
      {
        fprintf(f,"%s *p%c[%d] = {",_typeEnvToC(i),'0' + i,ptrs[i] - 1);

        for (uint16_t j = 0; j < ptrs[i] - 1; ++j)
        {
          if (first)
            first = 0;
          else
            fputc(',',f);

          fputc('m',f);
          fputc('0' + i,f);
        }

        fputs("}; // pointers\n",f);
      }
    }
  }
 
  if (needed & 0x0f)
    fputc('\n',f);

  uint32_t n = 0;
  instr = bytecode + CMN_BYTECODE_HEADER_SIZE;

  uint8_t needNewline = 0;

  while (*instr != CMN_OPCODE_END) // forward declare functions
  {
    if (instr[0] == CMN_OPCODE_DES && instr[1] == CMN_DES_FUNC)
    {
      fprintf(f,"void %s(void);\n",
        functionIndexToName(compiler,bytecodeGetBlock(bytecode,n) - 1,0));

      needNewline = 1;
    }

    instr += 2;
    n++;
  }

  if (needNewline)
    fputc('\n',f);
    
  const char *indentStr;

  for (uint8_t i = 0; i < 2; ++i) // two passes: 1st function, 2nd main
  {
    uint8_t inFunc = 0;
    n = 0;
    instr = bytecode + CMN_BYTECODE_HEADER_SIZE;
    indentStr = is + 127;

    if (i == 1)
    {
      fputs("int main(int argc, char **argv)\n"
        "{\n  gargc = argc;\n  gargv = argv;\n",f);

      indentStr -= 2;
    }

    while (*instr != CMN_OPCODE_END)
    {
      uint8_t pop = (instr[1] & CMN_MASK_INSTR_NOPOP) == 0;
      char typeEnvDigit = '0' + CMN_instrTypeEnv(instr);

      if (instr[0] == CMN_OPCODE_DES && instr[1] == CMN_DES_FUNC)
      {
        inFunc = 1;

        if (i == 0)
        {
          fprintf(f,"void %s(void)\n{\n",
            functionIndexToName(compiler,bytecodeGetBlock(bytecode,n) - 1,0));

          indentStr -= 2;
        }
      }  
      else if (instr[0] == CMN_OPCODE_RET)
      {
        inFunc = 0;
    
        if (i == 0)
        {
          fputs("}\n\n",f);
          indentStr += 2;
        }
      }
      else if (
        inFunc != i && // only print out the currently processed level
        instr[0] != CMN_OPCODE_NOP && // ignore these
        instr[0] != CMN_OPCODE_COC &&
        instr[0] != CMN_OPCODE_JIA &&
        instr[0] != CMN_OPCODE_JNA &&
        instr[0] != CMN_OPCODE_JMA)
        switch (instr[0])
        {
          case CMN_OPCODE_DES:
            switch (instr[1])
            {
              case CMN_DES_IF:
                fprintf(f,"%sif (*(st%c%s))\n%s{\n",indentStr,
                  '0' + CMN_instrTypeEnv(instr + 2),
                  (instr[3] & CMN_MASK_INSTR_NOPOP) ? "" : "--",
                  indentStr);
          
                indentStr -= 2;
                break;

              case CMN_DES_ELSE:
                indentStr += 2;
                fprintf(f,"%s}\n%selse\n%s{\n",
                  indentStr,indentStr,indentStr);
                indentStr -= 2;
                break;

              case CMN_DES_IF_END:
              case CMN_DES_LOOP_END:
                indentStr += 2;
                fprintf(f,"%s}\n",indentStr);
                break;

              case CMN_DES_LOOP:
                fprintf(f,"%swhile (",indentStr);

                if (instr[2] != CMN_OPCODE_JIA &&
                    instr[2] != CMN_OPCODE_JNA) // infinite?
                  fputc('1',f);
                else
                  fprintf(f,"*(st%c%s)",'0' + CMN_instrTypeEnv(instr + 2),
                    (instr[3] & CMN_MASK_INSTR_NOPOP) ? "" : "--");

                fprintf(f,")\n%s{\n",indentStr);
                indentStr -= 2;
                break;

              case CMN_DES_LOOP_BREAK:
                fprintf(f,"%sbreak;\n",indentStr);
                break;

              case CMN_DES_EXIT:
                fprintf(f,"%sreturn",indentStr);

                if (i == 1)
                  fputs(" 0",f);

                fputs(";\n",f);
                break;

              case CMN_DES_LABEL:
                fprintf(f,"label%u:\n",n + 1);
                break;

              case CMN_DES_GOTO:
              {
                uint32_t addr = CMN_instrGetConst(instr + 2);

                if (bytecodeGetBlock(bytecode,addr) != bytecodeGetBlock(bytecode,n))
                {
                  fputs("ERROR: goto jump in between functions",stderr);
                  return 1;
                }

                fprintf(f,"%sgoto label%u;\n",indentStr,addr);
                break;
              }

              default: break;
            }

            break;

          case CMN_OPCODE_PSC:
          {
            uint64_t c1, c2;
            CMN_instrGetConsts(instr,&c1,&c2);
            _ptrIndexToC(c1,helpertStr,CMN_instrTypeEnv(instr));
            fprintf(f,"%s%s = m%c + %lu;\n",indentStr,helpertStr,typeEnvDigit,c2);
            break;
          }

          case CMN_OPCODE_PAC:
          {
            int64_t c1, c2;
            CMN_instrGetConsts(instr,(uint64_t *) &c1,(uint64_t *) &c2);

            if (c2 >= 8) // apply 4bit two's complement
              c2 -= 16;

            _ptrIndexToC(c1,helpertStr,CMN_instrTypeEnv(instr));
            fprintf(f,"%s%s += %ld;\n",indentStr,helpertStr,c2);
            break;
          }

          case CMN_OPCODE_PAX:
          {
            uint16_t pointerIndex = CMN_instrGetConst(instr);

            _ptrIndexToC(pointerIndex,helpertStr,
              CMN_instrTypeEnv(instr));

            fprintf(f,"%s%s += u2s%c(st%c[0]);",indentStr,helpertStr,
              typeEnvDigit,typeEnvDigit);

            if (pointerIndex != 0 && pop)
              fprintf(f," st%c--;",typeEnvDigit);

            fputc('\n',f);
            break;
          }

          case CMN_OPCODE_PCO:
          {
            uint64_t c1, c2;
            CMN_instrGetConsts(instr,&c1,&c2);
            _ptrIndexToC(c1,helpertStr,CMN_instrTypeEnv(instr));

            fprintf(f,"%s%s = ",indentStr,helpertStr);

            _ptrIndexToC(c2,helpertStr,CMN_instrTypeEnv(instr));
            
            fprintf(f,"%s;\n",helpertStr);
            break;
          }

          case CMN_OPCODE_MEX:
          {
            _ptrIndexToC(CMN_instrGetConst(instr),helpertStr,
              CMN_instrTypeEnv(instr));

            fprintf(f,"%s*(%s) = *st%c;",indentStr,helpertStr,
              typeEnvDigit);

            if (pop)
              fprintf(f," st%c--;",typeEnvDigit);

            fputc('\n',f);
            break;
          }

          case CMN_OPCODE_MGE:
            _ptrIndexToC(CMN_instrGetConst(instr),helpertStr,
              CMN_instrTypeEnv(instr));

            fprintf(f,"%sst%c[1] = *%s; st%c++;\n",
              indentStr,typeEnvDigit,helpertStr,typeEnvDigit);
            break;

          case CMN_OPCODE_PUX:
            fprintf(f,"%sst%c[1] = st%c[-1 * ((int) st%c[0])]; st%c++;\n",
              indentStr,typeEnvDigit,typeEnvDigit,typeEnvDigit,typeEnvDigit);
            break;

          case CMN_OPCODE_PCM:
          {
            uint64_t c1, c2;
            char helpertStr2[32];

            CMN_instrGetConsts(instr,&c1,&c2);
            _ptrIndexToC(c1,helpertStr,CMN_instrTypeEnv(instr));
            _ptrIndexToC(c2,helpertStr2,CMN_instrTypeEnv(instr));

            fprintf(f,"%sst%c[1] = %s == %s ? 0 : (1 + (%s < %s)); st%c++;\n",
              indentStr,typeEnvDigit,helpertStr,helpertStr2,helpertStr2,
              helpertStr,typeEnvDigit);

            break;
          }

          case CMN_OPCODE_CAE:
            fprintf(f,"%s%s();\n",indentStr,
              functionIndexToName(compiler,CMN_instrGetConst(instr),1));

            break;

          case CMN_OPCODE_CON:
          {
            uint64_t strLiteralLen = instructionsAreStringLit(instr,5);

            if (strLiteralLen)
            {
              fprintf(f,"%s",indentStr);
              instr += 4 * (strLiteralLen - 1);

              for (int i = 0; i < ((int) strLiteralLen); ++i)
              {
                fprintf(f,"st%c[%d] = '%c'; ",
                  typeEnvDigit,((int) strLiteralLen) - i,
                  (int) CMN_instrGetConst(instr));
                instr -= 4;
              }

              instr += 4 * strLiteralLen;
              n += 2 * (strLiteralLen - 1);
              fprintf(f,"st%c += %d;\n",typeEnvDigit,(int) strLiteralLen);
            }
            else
            {
              fprintf(f,"%sst%c[1] = %lu;",indentStr,typeEnvDigit,
                CMN_instrGetConst(instr));

              if ((instr[1] & CMN_MASK_INSTR_NOPOP) != 0)
                fprintf(f," st%c++;",typeEnvDigit);

              fputc('\n',f);
            }

            break;
          }

          case CMN_OPCODE_POP:
            fprintf(f,"%sst%c -= %lu;\n",indentStr,typeEnvDigit,
              CMN_instrGetConst(instr) + 1);
            break;

          case CMN_OPCODE_SWP:
            if (pop)
              fprintf(f,"%sst%c[0] ^= st%c[-1]; "
                "st%c[-1] ^= st%c[0]; st%c[0] ^= st%c[-1];\n",indentStr,
                typeEnvDigit,typeEnvDigit,typeEnvDigit,typeEnvDigit,
                typeEnvDigit,typeEnvDigit);
            else
              fprintf(f,"%sst%c[1] = st%c[0]; st%c[2] = st%c[-1]; st%c += 2;\n",
                indentStr,typeEnvDigit,typeEnvDigit,typeEnvDigit,typeEnvDigit,
                typeEnvDigit);

            break;

          case CMN_OPCODE_TRA:
            fprintf(f,"%s*st%c = *st%c;",indentStr,
              (char) ('0' + CMN_instrGetConst(instr)),typeEnvDigit);

            if (pop)
              fprintf(f," st%c--;",typeEnvDigit);

            fputc('\n',f);
            break;

          case CMN_OPCODE_CND:
            fprintf(f,"%sst%c[%d] = st%c[-2] ? st%c[-1] : st%c[0];",
              indentStr,typeEnvDigit,pop ? -2 : 1,typeEnvDigit,typeEnvDigit,
              typeEnvDigit);

            fprintf(f," st%c += %d;",typeEnvDigit,pop ? -2 : 1);

            fputc('\n',f);
            break;

          case CMN_OPCODE_CAL:
          {
            uint32_t n = CMN_instrGetConst(instr) - 1;

            while (bytecode[CMN_BYTECODE_HEADER_SIZE + 2 * n] != CMN_OPCODE_DES)
              n--;

            fprintf(f,"%s%s();\n",indentStr,
              functionIndexToName(compiler,bytecodeGetBlock(bytecode,n) - 1,0));

            break;
          }

          case CMN_OPCODE_INI:
            fprintf(f,
              "%sfor (int i = 0; i < gargc; ++i) // push arguments\n%s{\n"
              "%s  const char *c = gargv[gargc - 1 - i], *c2 = c;\n"
              "%s  while (*c2 != 0) c2++;\n\n"
              "%s  while (c2 >= c)\n%s  {\n"
              "%s    st0[1] = *c2; st0++; c2--;\n%s  }\n%s}\n\n"
              "%sst0[1] = gargc; st0++;\n",
              indentStr,indentStr,indentStr,
              indentStr,indentStr,indentStr,
              indentStr,indentStr,indentStr,indentStr);
            break;

          case CMN_OPCODE_INU:
            fprintf(f,"%sst%c[1] = !eof; st%c++;\n",
              indentStr,typeEnvDigit,typeEnvDigit);
            break;

          case CMN_OPCODE_ADR:
            fprintf(f,"%sst%c[1] = st%c - m%c; st%c++;\n",
              indentStr,typeEnvDigit,typeEnvDigit,typeEnvDigit,typeEnvDigit);
            break;

          case CMN_OPCODE_INP:
            fprintf(f,"%sst%c[1] = in(); st%c++;\n",
              indentStr,typeEnvDigit,typeEnvDigit);
            break;

          case CMN_OPCODE_OUT:
            fprintf(f,"%sout(st%c[0]);",indentStr,typeEnvDigit);

            if (pop)
              fprintf(f," st%c--;",typeEnvDigit);

            fputc('\n',f);
            break;

          default:
          { 
            char arg1[64], arg2[64];
            int8_t stackShift;
            uint8_t opcodeGroup = instr[0] & CMN_MASK_INSTR_GROUP;

            _instrArgsToC(instr,arg2,arg1,&stackShift);

            fprintf(f,"%sst%c[%d] = ",indentStr,typeEnvDigit,stackShift);

            switch (opcodeGroup)
            {
#define OP(opc,str) case opc: fprintf(f,str,arg1,arg2); break;
              OP(CMN_OPCODE_AD,"%s + %s")
              OP(CMN_OPCODE_SU,"%s - %s")
              OP(CMN_OPCODE_MU,"%s * %s")
              OP(CMN_OPCODE_DI,"%s / %s")
              OP(CMN_OPCODE_MO,"%s %% %s")
              OP(CMN_OPCODE_GR,"%s > %s")
              OP(CMN_OPCODE_GE,"%s >= %s")
              OP(CMN_OPCODE_SM,"%s < %s")
              OP(CMN_OPCODE_SE,"%s <= %s")
              OP(CMN_OPCODE_EQ,"%s == %s")
              OP(CMN_OPCODE_NE,"%s != %s")
              OP(CMN_OPCODE_BA,"%s & %s")
              OP(CMN_OPCODE_BO,"%s | %s")
              OP(CMN_OPCODE_BX,"%s ^ %s")
              OP(CMN_OPCODE_LA,"%s && %s")
              OP(CMN_OPCODE_LO,"%s || %s")
              OP(CMN_OPCODE_LX,"(%s != 0) != (%s != 0)")
#undef OP
              case CMN_OPCODE_BNO & CMN_MASK_INSTR_GROUP:
                fprintf(f,"~%s",arg1);
                break;

              case CMN_OPCODE_SR:
              case CMN_OPCODE_SL:
                fprintf(f,"%s < sizeof(%s) * 8 ? %s %s %s : 0",
                  arg2,_typeEnvToC(CMN_instrTypeEnv(instr)),
                  arg1,opcodeGroup == CMN_OPCODE_SR ? ">>" : "<<",
                  arg2);
                break;

              case CMN_OPCODE_GS:
              case CMN_OPCODE_BS:
              case CMN_OPCODE_SS:
              case CMN_OPCODE_LS:
                fprintf(f,"u2s%c(%s) %s u2s%c(%s)",
                  typeEnvDigit,arg1,
                  opcodeGroup == CMN_OPCODE_GS ? ">" : (
                    opcodeGroup == CMN_OPCODE_BS ? ">=" : (
                      opcodeGroup == CMN_OPCODE_SS ? "<" : "<="
                  )),
                  typeEnvDigit,arg2);
                break;

              case CMN_OPCODE_DS:
              case CMN_OPCODE_MS:
                fprintf(f,"s2u%c(u2s%c(%s) %c u2s%c(%s))",
                  typeEnvDigit,typeEnvDigit,arg1,
                  opcodeGroup == CMN_OPCODE_DS ? '/' : '%',
                  typeEnvDigit,arg2);
                break;

              default:
                fputs("ERROR: unknown instruction.\n",stderr);
                return 0;
                break;
            }

            if (stackShift != 0)
              fprintf(f,"; st%c += %d",typeEnvDigit,stackShift);

            fputs(";\n",f);
            break;
          }
        }

      instr += 2;
      n++;
    }
  }

  fprintf(f,"%sreturn 0;\n}\n",indentStr);

  return 1;
}
